/**
* Copyright (C) 2012 Iordan Iordanov
* Copyright (C) 2010 Michael A. MacDonald
*
* This is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this software; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
* USA.
*/
//
// CanvasView is the Activity for showing VNC Desktop.
//
package com.iiordanov.bVNC;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.regex.Matcher;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.database.Cursor;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.SystemClock;
import android.support.v4.app.FragmentActivity;
import android.util.Log;
import android.view.Gravity;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnKeyListener;
import android.view.View.OnLongClickListener;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.Toast;
import com.iiordanov.android.bc.BCFactory;
import com.iiordanov.android.zoomer.ZoomControls;
import com.iiordanov.bVNC.dialogs.EnterTextDialog;
import com.iiordanov.bVNC.dialogs.MetaKeyDialog;
import com.iiordanov.bVNC.input.AbstractInputHandler;
import com.iiordanov.bVNC.input.Panner;
import com.iiordanov.bVNC.input.RemoteKeyboard;
import com.iiordanov.bVNC.input.SimulatedTouchpadInputHandler;
import com.iiordanov.bVNC.input.SingleHandedInputHandler;
import com.iiordanov.bVNC.input.TouchMouseDragPanInputHandler;
import com.iiordanov.bVNC.input.TouchMouseSwipePanInputHandler;
import com.iiordanov.bVNC.*;
import com.iiordanov.freebVNC.*;
import com.iiordanov.aRDP.*;
import com.iiordanov.freeaRDP.*;
import com.iiordanov.aSPICE.*;
import com.iiordanov.freeaSPICE.*;
public class RemoteCanvasActivity extends FragmentActivity implements OnKeyListener {
private final static String TAG = "RemoteCanvasActivity";
AbstractInputHandler inputHandler;
private RemoteCanvas canvas;
private Database database;
private MenuItem[] inputModeMenuItems;
private MenuItem[] scalingModeMenuItems;
private AbstractInputHandler inputModeHandlers[];
private ConnectionBean connection;
/* private static final int inputModeIds[] = { R.id.itemInputFitToScreen,
R.id.itemInputTouchpad,
R.id.itemInputMouse, R.id.itemInputPan,
R.id.itemInputTouchPanTrackballMouse,
R.id.itemInputDPadPanTouchMouse, R.id.itemInputTouchPanZoomMouse };
*/
private static final int inputModeIds[] = { R.id.itemInputTouchpad,
R.id.itemInputTouchPanZoomMouse,
R.id.itemInputDragPanZoomMouse,
R.id.itemInputSingleHanded };
private static final int scalingModeIds[] = { R.id.itemZoomable, R.id.itemFitToScreen,
R.id.itemOneToOne};
ZoomControls zoomer;
Panner panner;
SSHConnection sshConnection;
Handler handler;
RelativeLayout layoutKeys;
ImageButton keyStow;
ImageButton keyCtrl;
boolean keyCtrlToggled;
ImageButton keySuper;
boolean keySuperToggled;
ImageButton keyAlt;
boolean keyAltToggled;
ImageButton keyTab;
ImageButton keyEsc;
ImageButton keyShift;
boolean keyShiftToggled;
ImageButton keyUp;
ImageButton keyDown;
ImageButton keyLeft;
ImageButton keyRight;
boolean hardKeyboardExtended;
boolean extraKeysHidden = false;
int prevBottomOffset = 0;
/**
* Enables sticky immersive mode if supported.
*/
private void enableImmersive() {
if (Utils.querySharedPreferenceBoolean(this, Constants.disableImmersiveTag))
return;
if (Constants.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
canvas.setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
}
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
enableImmersive();
}
}
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
Utils.showMenu(this);
initialize();
if (connection != null && connection.isReadyForConnection())
continueConnecting();
}
void initialize () {
if (android.os.Build.VERSION.SDK_INT >= 9) {
android.os.StrictMode.ThreadPolicy policy = new android.os.StrictMode.ThreadPolicy.Builder().permitAll().build();
android.os.StrictMode.setThreadPolicy(policy);
}
handler = new Handler ();
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
if (Utils.querySharedPreferenceBoolean(this, Constants.keepScreenOnTag))
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
if (Utils.querySharedPreferenceBoolean(this, Constants.forceLandscapeTag))
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
database = ((App)getApplication()).getDatabase();
Intent i = getIntent();
connection = null;
Uri data = i.getData();
boolean isSupportedScheme = false;
if (data != null) {
String s = data.getScheme();
isSupportedScheme = s.equals("rdp") || s.equals("spice") || s.equals("vnc");
}
if (isSupportedScheme || !Utils.isNullOrEmptry(i.getType())) {
if (isMasterPasswordEnabled()) {
Utils.showFatalErrorMessage(this, getResources().getString(R.string.master_password_error_intents_not_supported));
return;
}
connection = ConnectionBean.createLoadFromUri(data, this);
String host = data.getHost();
if (!host.startsWith(Utils.getConnectionString(this))) {
connection.parseFromUri(data);
}
if (connection.isSaved()) {
connection.saveAndWriteRecent(false, database);
}
// we need to save the connection to display the loading screen, so otherwise we should exit
if (!connection.isReadyForConnection()) {
if (!connection.isSaved()) {
Log.i(TAG, "Exiting - Insufficent information to connect and connection was not saved.");
Toast.makeText(this, getString(R.string.error_uri_noinfo_nosave), Toast.LENGTH_LONG).show();;
} else {
// launch bVNC activity
Log.i(TAG, "Insufficent information to connect, showing connection dialog.");
Intent bVncIntent = new Intent(this, bVNC.class);
startActivity(bVncIntent);
}
finish();
return;
}
} else {
connection = new ConnectionBean(this);
Bundle extras = i.getExtras();
if (extras != null) {
connection.Gen_populate((ContentValues) extras.getParcelable(Utils.getConnectionString(this)));
}
// Parse a HOST:PORT entry but only if not ipv6 address
String host = connection.getAddress();
if (!Utils.isValidIpv6Address(host) && host.indexOf(':') > -1) {
String p = host.substring(host.indexOf(':') + 1);
try {
int parsedPort = Integer.parseInt(p);
connection.setPort(parsedPort);
connection.setAddress(host.substring(0, host.indexOf(':')));
} catch (Exception e) {}
}
if (connection.getPort() == 0)
connection.setPort(Constants.DEFAULT_PROTOCOL_PORT);
if (connection.getSshPort() == 0)
connection.setSshPort(Constants.DEFAULT_SSH_PORT);
}
}
void continueConnecting () {
// TODO: Implement left-icon
//requestWindowFeature(Window.FEATURE_LEFT_ICON);
//setFeatureDrawableResource(Window.FEATURE_LEFT_ICON, R.drawable.icon);
setContentView(R.layout.canvas);
canvas = (RemoteCanvas) findViewById(R.id.vnc_canvas);
zoomer = (ZoomControls) findViewById(R.id.zoomer);
// Initialize and define actions for on-screen keys.
initializeOnScreenKeys ();
canvas.initializeCanvas(connection, database, new Runnable() {
public void run() {
try { setModes(); } catch (NullPointerException e) { }
}
});
canvas.setOnKeyListener(this);
canvas.setFocusableInTouchMode(true);
canvas.setDrawingCacheEnabled(false);
// This code detects when the soft keyboard is up and sets an appropriate visibleHeight in vncCanvas.
// When the keyboard is gone, it resets visibleHeight and pans zero distance to prevent us from being
// below the desktop image (if we scrolled all the way down when the keyboard was up).
// TODO: Move this into a separate thread, and post the visibility changes to the handler.
// to avoid occupying the UI thread with this.
final View rootView = ((ViewGroup)findViewById(android.R.id.content)).getChildAt(0);
rootView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
Rect r = new Rect();
rootView.getWindowVisibleDisplayFrame(r);
// To avoid setting the visible height to a wrong value after an screen unlock event
// (when r.bottom holds the width of the screen rather than the height due to a rotation)
// we make sure r.top is zero (i.e. there is no notification bar and we are in full-screen mode)
// It's a bit of a hack.
if (r.top == 0) {
if (canvas.bitmapData != null) {
canvas.setVisibleHeight(r.bottom);
canvas.pan(0,0);
}
}
// Enable/show the zoomer if the keyboard is gone, and disable/hide otherwise.
// We detect the keyboard if more than 19% of the screen is covered.
int offset = 0;
int rootViewHeight = rootView.getHeight();
if (r.bottom > rootViewHeight*0.81) {
offset = rootViewHeight - r.bottom;
// Soft Kbd gone, shift the meta keys and arrows down.
if (layoutKeys != null) {
layoutKeys.offsetTopAndBottom(offset);
keyStow.offsetTopAndBottom(offset);
if (prevBottomOffset != offset) {
setExtraKeysVisibility(View.GONE, false);
canvas.invalidate();
zoomer.enable();
}
}
} else {
offset = r.bottom - rootViewHeight;
// Soft Kbd up, shift the meta keys and arrows up.
if (layoutKeys != null) {
layoutKeys.offsetTopAndBottom(offset);
keyStow.offsetTopAndBottom(offset);
if (prevBottomOffset != offset) {
setExtraKeysVisibility(View.VISIBLE, true);
canvas.invalidate();
zoomer.hide();
zoomer.disable();
}
}
}
setKeyStowDrawableAndVisibility();
prevBottomOffset = offset;
enableImmersive();
}
});
zoomer.hide();
zoomer.setOnZoomKeyboardClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
InputMethodManager inputMgr = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
inputMgr.toggleSoftInput(0, 0);
}
});
zoomer.setOnShowMenuClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
RemoteCanvasActivity.this.openOptionsMenu();
}
});
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT);
if (Utils.querySharedPreferenceBoolean(this, Constants.leftHandedModeTag)) {
params.gravity = Gravity.CENTER|Gravity.LEFT;
} else {
params.gravity = Gravity.CENTER|Gravity.RIGHT;
}
zoomer.setLayoutParams(params);
panner = new Panner(this, canvas.handler);
inputHandler = getInputHandlerById(R.id.itemInputTouchPanZoomMouse);
}
@SuppressWarnings("deprecation")
private void setKeyStowDrawableAndVisibility() {
Drawable replacer = null;
if (layoutKeys.getVisibility() == View.GONE)
replacer = getResources().getDrawable(R.drawable.showkeys);
else
replacer = getResources().getDrawable(R.drawable.hidekeys);
int sdk = android.os.Build.VERSION.SDK_INT;
if(sdk < android.os.Build.VERSION_CODES.JELLY_BEAN) {
keyStow.setBackgroundDrawable(replacer);
} else {
keyStow.setBackground(replacer);
}
if (connection.getExtraKeysToggleType() == Constants.EXTRA_KEYS_OFF)
keyStow.setVisibility(View.GONE);
else
keyStow.setVisibility(View.VISIBLE);
}
/**
* Initializes the on-screen keys for meta keys and arrow keys.
*/
private void initializeOnScreenKeys () {
layoutKeys = (RelativeLayout) findViewById(R.id.layoutKeys);
keyStow = (ImageButton) findViewById(R.id.keyStow);
setKeyStowDrawableAndVisibility();
keyStow.setOnClickListener(new OnClickListener () {
@Override
public void onClick(View arg0) {
if (layoutKeys.getVisibility() == View.VISIBLE) {
extraKeysHidden = true;
setExtraKeysVisibility(View.GONE, false);
} else {
extraKeysHidden = false;
setExtraKeysVisibility(View.VISIBLE, true);
}
layoutKeys.offsetTopAndBottom(prevBottomOffset);
setKeyStowDrawableAndVisibility();
}
});
// Define action of tab key and meta keys.
keyTab = (ImageButton) findViewById(R.id.keyTab);
keyTab.setOnTouchListener(new OnTouchListener () {
@Override
public boolean onTouch(View arg0, MotionEvent e) {
RemoteKeyboard k = canvas.getKeyboard();
int key = KeyEvent.KEYCODE_TAB;
if (e.getAction() == MotionEvent.ACTION_DOWN) {
BCFactory.getInstance().getBCHaptic().performLongPressHaptic(canvas);
keyTab.setImageResource(R.drawable.tabon);
k.repeatKeyEvent(key, new KeyEvent(e.getAction(), key));
return true;
} else if (e.getAction() == MotionEvent.ACTION_UP) {
keyTab.setImageResource(R.drawable.taboff);
resetOnScreenKeys (0);
k.stopRepeatingKeyEvent();
return true;
}
return false;
}
});
keyEsc = (ImageButton) findViewById(R.id.keyEsc);
keyEsc.setOnTouchListener(new OnTouchListener () {
@Override
public boolean onTouch(View arg0, MotionEvent e) {
RemoteKeyboard k = canvas.getKeyboard();
int key = 111; /* KEYCODE_ESCAPE */
if (e.getAction() == MotionEvent.ACTION_DOWN) {
BCFactory.getInstance().getBCHaptic().performLongPressHaptic(canvas);
keyEsc.setImageResource(R.drawable.escon);
k.repeatKeyEvent(key, new KeyEvent(e.getAction(), key));
return true;
} else if (e.getAction() == MotionEvent.ACTION_UP) {
keyEsc.setImageResource(R.drawable.escoff);
resetOnScreenKeys (0);
k.stopRepeatingKeyEvent();
return true;
}
return false;
}
});
keyCtrl = (ImageButton) findViewById(R.id.keyCtrl);
keyCtrl.setOnClickListener(new OnClickListener () {
@Override
public void onClick(View arg0) {
boolean on = canvas.getKeyboard().onScreenCtrlToggle();
keyCtrlToggled = false;
if (on)
keyCtrl.setImageResource(R.drawable.ctrlon);
else
keyCtrl.setImageResource(R.drawable.ctrloff);
}
});
keyCtrl.setOnLongClickListener(new OnLongClickListener () {
@Override
public boolean onLongClick(View arg0) {
BCFactory.getInstance().getBCHaptic().performLongPressHaptic(canvas);
boolean on = canvas.getKeyboard().onScreenCtrlToggle();
keyCtrlToggled = true;
if (on)
keyCtrl.setImageResource(R.drawable.ctrlon);
else
keyCtrl.setImageResource(R.drawable.ctrloff);
return true;
}
});
keySuper = (ImageButton) findViewById(R.id.keySuper);
keySuper.setOnClickListener(new OnClickListener () {
@Override
public void onClick(View arg0) {
boolean on = canvas.getKeyboard().onScreenSuperToggle();
keySuperToggled = false;
if (on)
keySuper.setImageResource(R.drawable.superon);
else
keySuper.setImageResource(R.drawable.superoff);
}
});
keySuper.setOnLongClickListener(new OnLongClickListener () {
@Override
public boolean onLongClick(View arg0) {
BCFactory.getInstance().getBCHaptic().performLongPressHaptic(canvas);
boolean on = canvas.getKeyboard().onScreenSuperToggle();
keySuperToggled = true;
if (on)
keySuper.setImageResource(R.drawable.superon);
else
keySuper.setImageResource(R.drawable.superoff);
return true;
}
});
keyAlt = (ImageButton) findViewById(R.id.keyAlt);
keyAlt.setOnClickListener(new OnClickListener () {
@Override
public void onClick(View arg0) {
boolean on = canvas.getKeyboard().onScreenAltToggle();
keyAltToggled = false;
if (on)
keyAlt.setImageResource(R.drawable.alton);
else
keyAlt.setImageResource(R.drawable.altoff);
}
});
keyAlt.setOnLongClickListener(new OnLongClickListener () {
@Override
public boolean onLongClick(View arg0) {
BCFactory.getInstance().getBCHaptic().performLongPressHaptic(canvas);
boolean on = canvas.getKeyboard().onScreenAltToggle();
keyAltToggled = true;
if (on)
keyAlt.setImageResource(R.drawable.alton);
else
keyAlt.setImageResource(R.drawable.altoff);
return true;
}
});
keyShift = (ImageButton) findViewById(R.id.keyShift);
keyShift.setOnClickListener(new OnClickListener () {
@Override
public void onClick(View arg0) {
boolean on = canvas.getKeyboard().onScreenShiftToggle();
keyShiftToggled = false;
if (on)
keyShift.setImageResource(R.drawable.shifton);
else
keyShift.setImageResource(R.drawable.shiftoff);
}
});
keyShift.setOnLongClickListener(new OnLongClickListener () {
@Override
public boolean onLongClick(View arg0) {
BCFactory.getInstance().getBCHaptic().performLongPressHaptic(canvas);
boolean on = canvas.getKeyboard().onScreenShiftToggle();
keyShiftToggled = true;
if (on)
keyShift.setImageResource(R.drawable.shifton);
else
keyShift.setImageResource(R.drawable.shiftoff);
return true;
}
});
// TODO: Evaluate whether I should instead be using:
// vncCanvas.sendMetaKey(MetaKeyBean.keyArrowLeft);
// Define action of arrow keys.
keyUp = (ImageButton) findViewById(R.id.keyUpArrow);
keyUp.setOnTouchListener(new OnTouchListener () {
@Override
public boolean onTouch(View arg0, MotionEvent e) {
RemoteKeyboard k = canvas.getKeyboard();
int key = KeyEvent.KEYCODE_DPAD_UP;
if (e.getAction() == MotionEvent.ACTION_DOWN) {
BCFactory.getInstance().getBCHaptic().performLongPressHaptic(canvas);
keyUp.setImageResource(R.drawable.upon);
k.repeatKeyEvent(key, new KeyEvent(e.getAction(), key));
return true;
} else if (e.getAction() == MotionEvent.ACTION_UP) {
keyUp.setImageResource(R.drawable.upoff);
resetOnScreenKeys (0);
k.stopRepeatingKeyEvent();
return true;
}
return false;
}
});
keyDown = (ImageButton) findViewById(R.id.keyDownArrow);
keyDown.setOnTouchListener(new OnTouchListener () {
@Override
public boolean onTouch(View arg0, MotionEvent e) {
RemoteKeyboard k = canvas.getKeyboard();
int key = KeyEvent.KEYCODE_DPAD_DOWN;
if (e.getAction() == MotionEvent.ACTION_DOWN) {
BCFactory.getInstance().getBCHaptic().performLongPressHaptic(canvas);
keyDown.setImageResource(R.drawable.downon);
k.repeatKeyEvent(key, new KeyEvent(e.getAction(), key));
return true;
} else if (e.getAction() == MotionEvent.ACTION_UP) {
keyDown.setImageResource(R.drawable.downoff);
resetOnScreenKeys (0);
k.stopRepeatingKeyEvent();
return true;
}
return false;
}
});
keyLeft = (ImageButton) findViewById(R.id.keyLeftArrow);
keyLeft.setOnTouchListener(new OnTouchListener () {
@Override
public boolean onTouch(View arg0, MotionEvent e) {
RemoteKeyboard k = canvas.getKeyboard();
int key = KeyEvent.KEYCODE_DPAD_LEFT;
if (e.getAction() == MotionEvent.ACTION_DOWN) {
BCFactory.getInstance().getBCHaptic().performLongPressHaptic(canvas);
keyLeft.setImageResource(R.drawable.lefton);
k.repeatKeyEvent(key, new KeyEvent(e.getAction(), key));
return true;
} else if (e.getAction() == MotionEvent.ACTION_UP) {
keyLeft.setImageResource(R.drawable.leftoff);
resetOnScreenKeys (0);
k.stopRepeatingKeyEvent();
return true;
}
return false;
}
});
keyRight = (ImageButton) findViewById(R.id.keyRightArrow);
keyRight.setOnTouchListener(new OnTouchListener () {
@Override
public boolean onTouch(View arg0, MotionEvent e) {
RemoteKeyboard k = canvas.getKeyboard();
int key = KeyEvent.KEYCODE_DPAD_RIGHT;
if (e.getAction() == MotionEvent.ACTION_DOWN) {
BCFactory.getInstance().getBCHaptic().performLongPressHaptic(canvas);
keyRight.setImageResource(R.drawable.righton);
k.repeatKeyEvent(key, new KeyEvent(e.getAction(), key));
return true;
} else if (e.getAction() == MotionEvent.ACTION_UP) {
keyRight.setImageResource(R.drawable.rightoff);
resetOnScreenKeys (0);
k.stopRepeatingKeyEvent();
return true;
}
return false;
}
});
}
/**
* Resets the state and image of the on-screen keys.
*/
private void resetOnScreenKeys (int keyCode) {
// Do not reset on-screen keys if keycode is SHIFT.
switch (keyCode) {
case KeyEvent.KEYCODE_SHIFT_LEFT:
case KeyEvent.KEYCODE_SHIFT_RIGHT: return;
}
if (!keyCtrlToggled) {
keyCtrl.setImageResource(R.drawable.ctrloff);
canvas.getKeyboard().onScreenCtrlOff();
}
if (!keyAltToggled) {
keyAlt.setImageResource(R.drawable.altoff);
canvas.getKeyboard().onScreenAltOff();
}
if (!keySuperToggled) {
keySuper.setImageResource(R.drawable.superoff);
canvas.getKeyboard().onScreenSuperOff();
}
if (!keyShiftToggled) {
keyShift.setImageResource(R.drawable.shiftoff);
canvas.getKeyboard().onScreenShiftOff();
}
}
/**
* Sets the visibility of the extra keys appropriately.
*/
private void setExtraKeysVisibility (int visibility, boolean forceVisible) {
Configuration config = getResources().getConfiguration();
//Log.e(TAG, "Hardware kbd hidden: " + Integer.toString(config.hardKeyboardHidden));
//Log.e(TAG, "Any keyboard hidden: " + Integer.toString(config.keyboardHidden));
//Log.e(TAG, "Keyboard type: " + Integer.toString(config.keyboard));
boolean makeVisible = forceVisible;
if (config.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO)
makeVisible = true;
if (!extraKeysHidden && makeVisible &&
connection.getExtraKeysToggleType() == Constants.EXTRA_KEYS_ON) {
layoutKeys.setVisibility(View.VISIBLE);
layoutKeys.invalidate();
return;
}
if (visibility == View.GONE) {
layoutKeys.setVisibility(View.GONE);
layoutKeys.invalidate();
}
}
/*
* TODO: REMOVE THIS AS SOON AS POSSIBLE.
* onPause: This is an ugly hack for the Playbook, because the Playbook hides the keyboard upon unlock.
* This causes the visible height to remain less, as if the soft keyboard is still up. This hack must go
* away as soon as the Playbook doesn't need it anymore.
*/
@Override
protected void onPause(){
super.onPause();
try {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(canvas.getWindowToken(), 0);
} catch (NullPointerException e) { }
}
/*
* TODO: REMOVE THIS AS SOON AS POSSIBLE.
* onResume: This is an ugly hack for the Playbook which hides the keyboard upon unlock. This causes the visible
* height to remain less, as if the soft keyboard is still up. This hack must go away as soon
* as the Playbook doesn't need it anymore.
*/
@Override
protected void onResume(){
super.onResume();
Log.i(TAG, "onResume called.");
try {
canvas.postInvalidateDelayed(600);
} catch (NullPointerException e) { }
}
/**
* Set modes on start to match what is specified in the ConnectionBean;
* color mode (already done) scaling, input mode
*/
void setModes() {
AbstractInputHandler handler = getInputHandlerByName(connection.getInputMode());
AbstractScaling.getByScaleType(connection.getScaleMode()).setScaleTypeForActivity(this);
this.inputHandler = handler;
showPanningState(false);
}
/*
* (non-Javadoc)
*
* @see android.app.Activity#onCreateDialog(int)
*/
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case R.layout.entertext:
return new EnterTextDialog(this);
case R.id.itemHelpInputMode:
return createHelpDialog ();
}
// Default to meta key dialog
return new MetaKeyDialog(this);
}
/**
* Creates the help dialog for this activity.
*/
private Dialog createHelpDialog() {
AlertDialog.Builder adb = new AlertDialog.Builder(this)
.setMessage(R.string.input_mode_help_text)
.setPositiveButton(R.string.close,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int whichButton) {
// We don't have to do anything.
}
});
Dialog d = adb.setView(new ListView (this)).create();
WindowManager.LayoutParams lp = new WindowManager.LayoutParams();
lp.copyFrom(d.getWindow().getAttributes());
lp.width = WindowManager.LayoutParams.MATCH_PARENT;
lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
d.show();
d.getWindow().setAttributes(lp);
return d;
}
/*
* (non-Javadoc)
*
* @see android.app.Activity#onPrepareDialog(int, android.app.Dialog)
*/
@SuppressWarnings("deprecation")
@Override
protected void onPrepareDialog(int id, Dialog dialog) {
super.onPrepareDialog(id, dialog);
if (dialog instanceof ConnectionSettable)
((ConnectionSettable) dialog).setConnection(connection);
}
/**
* This runnable fixes things up after a rotation.
*/
private Runnable rotationCorrector = new Runnable() {
public void run() {
try { correctAfterRotation (); } catch (NullPointerException e) { }
}
};
/**
* This function is called by the rotationCorrector runnable
* to fix things up after a rotation.
*/
private void correctAfterRotation () {
// Its quite common to see NullPointerExceptions here when this function is called
// at the point of disconnection. Hence, we catch and ignore the error.
float oldScale = canvas.scaling.getScale();
int x = canvas.absoluteXPosition;
int y = canvas.absoluteYPosition;
canvas.scaling.setScaleTypeForActivity(RemoteCanvasActivity.this);
float newScale = canvas.scaling.getScale();
canvas.scaling.adjust(this, oldScale/newScale, 0, 0);
newScale = canvas.scaling.getScale();
if (newScale <= oldScale) {
canvas.absoluteXPosition = x;
canvas.absoluteYPosition = y;
canvas.scrollToAbsolute();
}
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
try {
setExtraKeysVisibility(View.GONE, false);
// Correct a few times just in case. There is no visual effect.
handler.postDelayed(rotationCorrector, 300);
handler.postDelayed(rotationCorrector, 600);
handler.postDelayed(rotationCorrector, 1200);
} catch (NullPointerException e) { }
}
@Override
protected void onStart() {
super.onStart();
try {
canvas.postInvalidateDelayed(800);
} catch (NullPointerException e) { }
}
@Override
protected void onStop() {
super.onStop();
}
@Override
protected void onRestart() {
super.onRestart();
try {
canvas.postInvalidateDelayed(1000);
} catch (NullPointerException e) { }
}
/** {@inheritDoc} */
@Override
public boolean onCreateOptionsMenu(Menu menu) {
try {
getMenuInflater().inflate(R.menu.vnccanvasactivitymenu, menu);
menu.findItem(canvas.scaling.getId()).setChecked(true);
Menu inputMenu = menu.findItem(R.id.itemInputMode).getSubMenu();
inputModeMenuItems = new MenuItem[inputModeIds.length];
for (int i = 0; i < inputModeIds.length; i++) {
inputModeMenuItems[i] = inputMenu.findItem(inputModeIds[i]);
}
updateInputMenu();
Menu scalingMenu = menu.findItem(R.id.itemScaling).getSubMenu();
scalingModeMenuItems = new MenuItem[scalingModeIds.length];
for (int i = 0; i < scalingModeIds.length; i++) {
scalingModeMenuItems[i] = scalingMenu.findItem(scalingModeIds[i]);
}
updateScalingMenu();
// Set the text of the Extra Keys menu item appropriately.
if (connection.getExtraKeysToggleType() == Constants.EXTRA_KEYS_ON)
menu.findItem(R.id.itemExtraKeys).setTitle(R.string.extra_keys_disable);
else
menu.findItem(R.id.itemExtraKeys).setTitle(R.string.extra_keys_enable);
/* menu.findItem(R.id.itemFollowMouse).setChecked(
connection.getFollowMouse());
menu.findItem(R.id.itemFollowPan).setChecked(connection.getFollowPan());
*/
/* TODO: This is how one detects long-presses on menu items. However, getActionView is not available in Android 2.3...
menu.findItem(R.id.itemExtraKeys).getActionView().setOnLongClickListener(new OnLongClickListener () {
@Override
public boolean onLongClick(View arg0) {
Toast.makeText(arg0.getContext(), "Long Press Detected.", Toast.LENGTH_LONG).show();
return false;
}
});
*/
} catch (NullPointerException e) { }
return true;
}
/**
* Change the scaling mode sub-menu to reflect available scaling modes.
*/
void updateScalingMenu() {
try {
for (MenuItem item : scalingModeMenuItems) {
// If the entire framebuffer is NOT contained in the bitmap, fit-to-screen is meaningless.
if (item.getItemId() == R.id.itemFitToScreen) {
if ( canvas != null && canvas.bitmapData != null &&
(canvas.bitmapData.bitmapheight != canvas.bitmapData.framebufferheight ||
canvas.bitmapData.bitmapwidth != canvas.bitmapData.framebufferwidth) )
item.setEnabled(false);
else
item.setEnabled(true);
} else
item.setEnabled(true);
}
} catch (NullPointerException e) { }
}
/**
* Change the input mode sub-menu to reflect change in scaling
*/
void updateInputMenu() {
try {
for (MenuItem item : inputModeMenuItems) {
item.setEnabled(canvas.scaling.isValidInputMode(item.getItemId()));
if (getInputHandlerById(item.getItemId()) == inputHandler)
item.setChecked(true);
}
} catch (NullPointerException e) { }
}
/**
* If id represents an input handler, return that; otherwise return null
*
* @param id
* @return
*/
AbstractInputHandler getInputHandlerById(int id) {
boolean isRdp = getPackageName().contains("RDP");
if (inputModeHandlers == null) {
inputModeHandlers = new AbstractInputHandler[inputModeIds.length];
}
for (int i = 0; i < inputModeIds.length; ++i) {
if (inputModeIds[i] == id) {
if (inputModeHandlers[i] == null) {
switch (id) {
/* case R.id.itemInputFitToScreen:
inputModeHandlers[i] = new FitToScreenMode();
break;
case R.id.itemInputPan:
inputModeHandlers[i] = new PanMode();
break;
case R.id.itemInputTouchPanTrackballMouse:
inputModeHandlers[i] = new TouchPanTrackballMouse();
break;
case R.id.itemInputMouse:
inputModeHandlers[i] = new MouseMode();
break;
case R.id.itemInputDPadPanTouchMouse:
inputModeHandlers[i] = new DPadPanTouchMouseMode();
break;
*/
case R.id.itemInputTouchPanZoomMouse:
inputModeHandlers[i] = new TouchMouseSwipePanInputHandler(this, canvas, isRdp);
break;
case R.id.itemInputDragPanZoomMouse:
inputModeHandlers[i] = new TouchMouseDragPanInputHandler(this, canvas, isRdp);
break;
case R.id.itemInputTouchpad:
inputModeHandlers[i] = new SimulatedTouchpadInputHandler(this, canvas, isRdp);
break;
case R.id.itemInputSingleHanded:
inputModeHandlers[i] = new SingleHandedInputHandler(this, canvas, isRdp);
break;
}
}
return inputModeHandlers[i];
}
}
return null;
}
void clearInputHandlers() {
if (inputModeHandlers == null)
return;
for (int i = 0; i < inputModeIds.length; ++i) {
inputModeHandlers[i] = null;
}
inputModeHandlers = null;
}
AbstractInputHandler getInputHandlerByName(String name) {
AbstractInputHandler result = null;
for (int id : inputModeIds) {
AbstractInputHandler handler = getInputHandlerById(id);
if (handler.getName().equals(name)) {
result = handler;
break;
}
}
if (result == null) {
result = getInputHandlerById(R.id.itemInputTouchPanZoomMouse);
}
return result;
}
int getModeIdFromHandler(AbstractInputHandler handler) {
for (int id : inputModeIds) {
if (handler == getInputHandlerById(id))
return id;
}
return R.id.itemInputTouchPanZoomMouse;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
canvas.getKeyboard().setAfterMenu(true);
switch (item.getItemId()) {
case R.id.itemInfo:
canvas.showConnectionInfo();
return true;
case R.id.itemSpecialKeys:
showDialog(R.layout.metakey);
return true;
case R.id.itemColorMode:
selectColorModel();
return true;
// Following sets one of the scaling options
case R.id.itemZoomable:
case R.id.itemOneToOne:
case R.id.itemFitToScreen:
AbstractScaling.getById(item.getItemId()).setScaleTypeForActivity(this);
item.setChecked(true);
showPanningState(false);
return true;
case R.id.itemCenterMouse:
canvas.getPointer().warpMouse(canvas.absoluteXPosition + canvas.getVisibleWidth() / 2,
canvas.absoluteYPosition + canvas.getVisibleHeight() / 2);
return true;
case R.id.itemDisconnect:
canvas.closeConnection();
finish();
return true;
case R.id.itemEnterText:
showDialog(R.layout.entertext);
return true;
case R.id.itemCtrlAltDel:
canvas.getKeyboard().sendMetaKey(MetaKeyBean.keyCtrlAltDel);
return true;
/* case R.id.itemFollowMouse:
boolean newFollow = !connection.getFollowMouse();
item.setChecked(newFollow);
connection.setFollowMouse(newFollow);
if (newFollow) {
vncCanvas.panToMouse();
}
connection.save(database.getWritableDatabase());
database.close();
return true;
case R.id.itemFollowPan:
boolean newFollowPan = !connection.getFollowPan();
item.setChecked(newFollowPan);
connection.setFollowPan(newFollowPan);
connection.save(database.getWritableDatabase());
database.close();
return true;
case R.id.itemArrowLeft:
vncCanvas.sendMetaKey(MetaKeyBean.keyArrowLeft);
return true;
case R.id.itemArrowUp:
vncCanvas.sendMetaKey(MetaKeyBean.keyArrowUp);
return true;
case R.id.itemArrowRight:
vncCanvas.sendMetaKey(MetaKeyBean.keyArrowRight);
return true;
case R.id.itemArrowDown:
vncCanvas.sendMetaKey(MetaKeyBean.keyArrowDown);
return true;
*/
case R.id.itemSendKeyAgain:
sendSpecialKeyAgain();
return true;
// Disabling Manual/Wiki Menu item as the original does not correspond to this project anymore.
//case R.id.itemOpenDoc:
// Utils.showDocumentation(this);
// return true;
case R.id.itemExtraKeys:
if (connection.getExtraKeysToggleType() == Constants.EXTRA_KEYS_ON) {
connection.setExtraKeysToggleType(Constants.EXTRA_KEYS_OFF);
item.setTitle(R.string.extra_keys_enable);
setExtraKeysVisibility(View.GONE, false);
} else {
connection.setExtraKeysToggleType(Constants.EXTRA_KEYS_ON);
item.setTitle(R.string.extra_keys_disable);
setExtraKeysVisibility(View.VISIBLE, false);
extraKeysHidden = false;
}
setKeyStowDrawableAndVisibility();
connection.save(database.getWritableDatabase());
database.close();
return true;
case R.id.itemHelpInputMode:
showDialog(R.id.itemHelpInputMode);
return true;
default:
AbstractInputHandler input = getInputHandlerById(item.getItemId());
if (input != null) {
inputHandler = input;
connection.setInputMode(input.getName());
if (input.getName().equals(SimulatedTouchpadInputHandler.TOUCHPAD_MODE)) {
connection.setFollowMouse(true);
connection.setFollowPan(true);
} else {
connection.setFollowMouse(false);
connection.setFollowPan(false);
}
item.setChecked(true);
showPanningState(true);
connection.save(database.getWritableDatabase());
database.close();
return true;
}
}
return super.onOptionsItemSelected(item);
}
private MetaKeyBean lastSentKey;
private void sendSpecialKeyAgain() {
if (lastSentKey == null
|| lastSentKey.get_Id() != connection.getLastMetaKeyId()) {
ArrayList<MetaKeyBean> keys = new ArrayList<MetaKeyBean>();
Cursor c = database.getReadableDatabase().rawQuery(
MessageFormat.format("SELECT * FROM {0} WHERE {1} = {2}",
MetaKeyBean.GEN_TABLE_NAME,
MetaKeyBean.GEN_FIELD__ID, connection
.getLastMetaKeyId()),
MetaKeyDialog.EMPTY_ARGS);
MetaKeyBean.Gen_populateFromCursor(c, keys, MetaKeyBean.NEW);
c.close();
if (keys.size() > 0) {
lastSentKey = keys.get(0);
} else {
lastSentKey = null;
}
}
if (lastSentKey != null)
canvas.getKeyboard().sendMetaKey(lastSentKey);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (canvas != null)
canvas.closeConnection();
if (database != null)
database.close();
canvas = null;
connection = null;
database = null;
zoomer = null;
panner = null;
clearInputHandlers();
inputHandler = null;
System.gc();
}
@Override
public boolean onKey(View v, int keyCode, KeyEvent evt) {
boolean consumed = false;
if (keyCode == KeyEvent.KEYCODE_MENU) {
if (evt.getAction() == KeyEvent.ACTION_DOWN)
return super.onKeyDown(keyCode, evt);
else
return super.onKeyUp(keyCode, evt);
}
try {
if (evt.getAction() == KeyEvent.ACTION_DOWN || evt.getAction() == KeyEvent.ACTION_MULTIPLE) {
consumed = inputHandler.onKeyDown(keyCode, evt);
} else if (evt.getAction() == KeyEvent.ACTION_UP){
consumed = inputHandler.onKeyUp(keyCode, evt);
}
resetOnScreenKeys (keyCode);
} catch (NullPointerException e) { }
return consumed;
}
public void showPanningState(boolean showLonger) {
if (showLonger) {
final Toast t = Toast.makeText(this, inputHandler.getHandlerDescription(), Toast.LENGTH_LONG);
TimerTask tt = new TimerTask () {
@Override
public void run() {
t.show();
try { Thread.sleep(2000); } catch (InterruptedException e) { }
t.show();
}};
new Timer ().schedule(tt, 2000);
t.show();
} else {
Toast t = Toast.makeText(this, inputHandler.getHandlerDescription(), Toast.LENGTH_SHORT);
t.show();
}
}
/*
* (non-Javadoc)
*
* @see android.app.Activity#onTrackballEvent(android.view.MotionEvent)
*/
@Override
public boolean onTrackballEvent(MotionEvent event) {
try {
// If we are using the Dpad as arrow keys, don't send the event to the inputHandler.
if (connection.getUseDpadAsArrows())
return false;
return inputHandler.onTrackballEvent(event);
} catch (NullPointerException e) { }
return super.onTrackballEvent(event);
}
// Send touch events or mouse events like button clicks to be handled.
@Override
public boolean onTouchEvent(MotionEvent event) {
try {
return inputHandler.onTouchEvent(event);
} catch (NullPointerException e) { }
return super.onTouchEvent(event);
}
// Send e.g. mouse events like hover and scroll to be handled.
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
// Ignore TOOL_TYPE_FINGER events that come from the touchscreen with HOVER type action
// which cause pointer jumping trouble in simulated touchpad for some devices.
int a = event.getAction();
if (! ( (a == MotionEvent.ACTION_HOVER_ENTER ||
a == MotionEvent.ACTION_HOVER_EXIT ||
a == MotionEvent.ACTION_HOVER_MOVE) &&
event.getSource() == InputDevice.SOURCE_TOUCHSCREEN &&
event.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER
) ) {
try {
return inputHandler.onTouchEvent(event);
} catch (NullPointerException e) { }
}
return super.onGenericMotionEvent(event);
}
private void selectColorModel() {
String[] choices = new String[COLORMODEL.values().length];
int currentSelection = -1;
for (int i = 0; i < choices.length; i++) {
COLORMODEL cm = COLORMODEL.values()[i];
choices[i] = cm.toString();
if (canvas.isColorModel(cm))
currentSelection = i;
}
final Dialog dialog = new Dialog(this);
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
ListView list = new ListView(this);
list.setAdapter(new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_checked, choices));
list.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
list.setItemChecked(currentSelection, true);
list.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
dialog.dismiss();
COLORMODEL cm = COLORMODEL.values()[arg2];
canvas.setColorModel(cm);
connection.setColorModel(cm.nameString());
connection.save(database.getWritableDatabase());
database.close();
Toast.makeText(RemoteCanvasActivity.this, getString(R.string.info_update_color_model_to) + cm.toString(), Toast.LENGTH_SHORT).show();
}
});
dialog.setContentView(list);
dialog.show();
}
long hideZoomAfterMs;
static final long ZOOM_HIDE_DELAY_MS = 2500;
HideZoomRunnable hideZoomInstance = new HideZoomRunnable();
public void stopPanner() {
panner.stop ();
}
public void showZoomer(boolean force) {
if (force || zoomer.getVisibility() != View.VISIBLE) {
zoomer.show();
hideZoomAfterMs = SystemClock.uptimeMillis() + ZOOM_HIDE_DELAY_MS;
canvas.handler.postAtTime(hideZoomInstance, hideZoomAfterMs + 10);
}
}
private class HideZoomRunnable implements Runnable {
public void run() {
if (SystemClock.uptimeMillis() >= hideZoomAfterMs) {
zoomer.hide();
}
}
}
public ConnectionBean getConnection() {
return connection;
}
// Returns whether we are using D-pad/Trackball to send arrow key events.
public boolean getUseDpadAsArrows() {
return connection.getUseDpadAsArrows();
}
// Returns whether the D-pad should be rotated to accommodate BT keyboards paired with phones.
public boolean getRotateDpad() {
return connection.getRotateDpad();
}
public float getSensitivity() {
// TODO: Make this a slider config option.
return 2.0f;
}
public boolean getAccelerationEnabled() {
// TODO: Make this a config option.
return true;
}
public Database getDatabase() {
return database;
}
public void setDatabase(Database database) {
this.database = database;
}
public RemoteCanvas getCanvas() {
return canvas;
}
public void setCanvas(RemoteCanvas vncCanvas) {
this.canvas = vncCanvas;
}
public Panner getPanner() {
return panner;
}
public void setPanner(Panner panner) {
this.panner = panner;
}
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putString("WORKAROUND_FOR_BUG_19917_KEY", "WORKAROUND_FOR_BUG_19917_VALUE");
super.onSaveInstanceState(outState);
}
private boolean isMasterPasswordEnabled() {
SharedPreferences sp = getSharedPreferences(Constants.generalSettingsTag, Context.MODE_PRIVATE);
return sp.getBoolean(Constants.masterPasswordEnabledTag, false);
}
}